[#10772] feat(authorization): refactor JcasbinAuthorizer with version-validated cache#10996
[#10772] feat(authorization): refactor JcasbinAuthorizer with version-validated cache#10996yuqi1129 wants to merge 29 commits into
Conversation
Code Coverage Report
Files
|
bd8bc8c to
a6a9bfc
Compare
### What changes were proposed in this pull request? This PR splits the group-related authorization cache mapper work from #10996. It adds: - `group_meta.updated_at` to H2/MySQL/PostgreSQL 1.3.0 schemas and 1.2.0-to-1.3.0 upgrade scripts. - Group updated-at mapper APIs and SQL providers. - `GroupUpdatedAt` PO. - `GroupMetaService.updateGroup` updates `group_meta.updated_at` when group role assignments change. - Mapper and service tests for group updated-at behavior. ### Why are the changes needed? Group role cache validation needs a DB-side version sentinel, matching the existing user/role cache pattern. Fix: #10770 ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? ```bash ./gradlew :core:spotlessApply ./gradlew :core:test --tests org.apache.gravitino.storage.relational.mapper.provider.base.TestAuthMappers --tests org.apache.gravitino.storage.relational.service.TestGroupMetaService ```
There was a problem hiding this comment.
Pull request overview
Refactors the server authorization path to introduce request-scoped deduplication (AuthorizationRequestContext) and a new cache abstraction (GravitinoCache) while reworking JcasbinAuthorizer to use version-validated caches plus an HA-oriented change poller for targeted invalidation.
Changes:
- Thread
AuthorizationRequestContextthrough request interception + authorization executors to enable per-request dedup and shared logging context. - Rewrite
JcasbinAuthorizercaching: version-validated user/group/role caches, metadata-id + owner caches, and a scheduled poller draining change logs for invalidation. - Add new cache abstraction + implementations/tests, and migrate change-log/owner polling queries to id-based cursors with supporting mapper + test updates.
Reviewed changes
Copilot reviewed 38 out of 38 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java | Updates mocks for new checkCurrentUser signature and authorizer interface changes. |
| server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java | Creates and threads a per-request AuthorizationRequestContext through user validation and authorization execution. |
| server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java | Updates executor interface to accept a request context and stores original expression into it. |
| server/src/main/java/org/apache/gravitino/server/web/filter/authorization/CommonAuthorizerExecutor.java | Passes a shared request context into expression evaluation (instead of allocating a new one). |
| server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizationExecutor.java | Changes executor contract to require AuthorizationRequestContext. |
| server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java | Adapts to the new executor signature and reuses caller-provided request context. |
| server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java | Adapts to the new executor signature and reuses caller-provided request context. |
| server-common/src/test/java/org/apache/gravitino/server/authorization/TestPassThroughAuthorizer.java | Updates tests for new isMetalakeUser(..., AuthorizationRequestContext) signature. |
| server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java | Updates mock authorizer to implement new isMetalakeUser signature. |
| server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizerCacheHelpers.java | Adds unit tests for cache-key building helpers and cached role snapshot DTOs. |
| server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java | Refactors tests to align with new mapper-based/version-validated cache behavior. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java | Updates authorizer interface implementation to accept request context. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java | Major cache refactor: version-validated role caches, metadata-id cache, owner cache, and change poller-driven invalidation. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/CachedUserRoles.java | Adds cached snapshot class for user direct role IDs keyed by user_meta.updated_at. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/CachedGroupRoles.java | Adds cached snapshot class for group role IDs keyed by group_meta.updated_at. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConverter.java | Updates OGNL expression generation to pass authorizationContext into isMetalakeUser. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java | Updates Javadoc reference for new isMetalakeUser signature. |
| server-common/build.gradle.kts | Adds Lombok processor/compileOnly deps for server-common sources/tests. |
| core/src/test/java/org/apache/gravitino/storage/relational/service/TestTableMetaService.java | Migrates entity change log polling tests from timestamp cursor to id cursor. |
| core/src/test/java/org/apache/gravitino/storage/relational/service/TestEntityChangeLogService.java | Migrates change-log service tests from timestamp cursor to id cursor. |
| core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestEntityChangeLogMapper.java | Updates mapper test for new id-based selectEntityChanges signature. |
| core/src/test/java/org/apache/gravitino/storage/relational/mapper/provider/base/TestAuthMappers.java | Updates owner-change polling test for new id-based cursor behavior. |
| core/src/test/java/org/apache/gravitino/cache/TestGravitinoCache.java | Adds unit tests for GravitinoCache implementations (TTL/eviction/prefix invalidation). |
| core/src/test/java/org/apache/gravitino/authorization/TestAuthorizationRequestContext.java | Adds tests for request-scoped dedup helpers and absent-result caching. |
| core/src/main/java/org/apache/gravitino/storage/relational/po/auth/ChangedOwnerInfo.java | Adds id field to support id-based owner change polling cursor. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java | Switches owner-change polling query to id-based cursor and adds selectLatestChangeId. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/EntityChangeLogBaseSQLProvider.java | Switches change-log polling query to id-based cursor and adds selectLatestChangeId. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaSQLProviderFactory.java | Exposes updated SQL provider methods for owner change polling and latest id. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/OwnerMetaMapper.java | Updates mapper API for id-based owner polling and latest id. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogSQLProviderFactory.java | Exposes updated SQL provider methods for change-log polling and latest id. |
| core/src/main/java/org/apache/gravitino/storage/relational/mapper/EntityChangeLogMapper.java | Updates mapper API for id-based polling and latest id. |
| core/src/main/java/org/apache/gravitino/Configs.java | Adds configuration for metadataId cache size and change poll interval. |
| core/src/main/java/org/apache/gravitino/cache/NoOpsGravitinoCache.java | Adds no-op cache implementation for disabling caching. |
| core/src/main/java/org/apache/gravitino/cache/GravitinoCache.java | Introduces cache abstraction used by authorization subsystem. |
| core/src/main/java/org/apache/gravitino/cache/CaffeineGravitinoCache.java | Adds Caffeine-backed GravitinoCache implementation. |
| core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java | Updates authorizer interface: isMetalakeUser now takes request context; adds structural-change hook. |
| core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java | Threads request context into checkCurrentUser and keeps backward-compatible overload. |
| core/src/main/java/org/apache/gravitino/authorization/AuthorizationRequestContext.java | Adds per-request caches for user/group/metadataId/owner and makes AuthorizationKey immutable via Lombok. |
|
Is it possible to split this PR, it is too big to review. |
|
Let's merge #11033 first, as this one is quite large to review |
### What changes were proposed in this pull request? This PR extracts the infrastructure part from #10996 to support the follow-up JcasbinAuthorizer cache refactor. It includes: - Add a generic `GravitinoCache` abstraction with Caffeine and no-op implementations. - Add per-request lookup caches in `AuthorizationRequestContext` for user, group, metadata id, and owner lookup deduplication. - Thread `AuthorizationRequestContext` through authorization filter/executor paths. - Update `isMetalakeUser` to accept `AuthorizationRequestContext`. - Switch entity/owner change polling mapper APIs to id-based cursors and expose latest id lookup helpers. This PR intentionally does not rewrite `JcasbinAuthorizer` cache behavior. The JcasbinAuthorizer version-validated cache refactor will stay in the follow-up PR. ### Why are the changes needed? This reduces the size of #10996 and provides the reusable support layer needed by the JcasbinAuthorizer cache refactor. Fix: #11032 ### Does this PR introduce _any_ user-facing change? No. ### How was this patch tested? - `./gradlew :core:spotlessApply :server-common:spotlessApply :server:spotlessApply` - `./gradlew :core:test --tests org.apache.gravitino.authorization.TestAuthorizationRequestContext --tests org.apache.gravitino.cache.TestGravitinoCache --tests org.apache.gravitino.storage.relational.mapper.provider.base.TestAuthMappers --tests org.apache.gravitino.storage.relational.mapper.provider.base.TestEntityChangeLogMapper --tests org.apache.gravitino.storage.relational.service.TestEntityChangeLogService --tests org.apache.gravitino.storage.relational.service.TestTableMetaService :server-common:test --tests org.apache.gravitino.server.authorization.TestPassThroughAuthorizer :server:test --tests org.apache.gravitino.server.web.filter.TestGravitinoInterceptionService`
…st isolation Address review polish on PR apache#10996: - Add a class-level doc on JcasbinAuthorizer summarising the three cache families (per-request dedup, version-validated, eventual-consistency) so reviewers do not have to read the entire 1100-line file to get the consistency model. - Document the id-based high-water cursor's "in-flight commit" trade-off next to the cursor init, and the entity_change_log writer contract (pre-mutation full_name) next to pollEntityChanges. - Note in Configs.GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE that the same value drives the user-role, group-role, and loaded-role caches so operators can size accordingly. - TestJcasbinAuthorizer now builds a fresh JcasbinAuthorizer per test in @beforeeach and closes it in @AfterEach so enforcer g-rows and version-validated cache state never bleed across cases regardless of JUnit execution order. Follow-up tracked in apache#11088 (route isSelf(ROLE) through the version-validated cache). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
roryqi
left a comment
There was a problem hiding this comment.
Could u extract a dedicated class to handle the function to handle polling the expired data?
| cache.cleanUp(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This class is so big, I suggest that we can split into several small classes.
There was a problem hiding this comment.
I see. let me split it again.
… JcasbinAuthorizer caches Introduces the HA invalidation infrastructure that will later carry the version-validated role caching (tracked in apache#10772). This change leaves the existing role-loading path on its current TTL-only behavior. Changes ------- * `JcasbinAuthorizationLookups`: two-tier metadata-id and owner lookup facade (per-request dedup via AuthorizationRequestContext, shared Caffeine-backed GravitinoCache fallback, DB on miss). Owners are now fetched directly from `owner_meta` via OwnerMetaMapper instead of the OWNER_REL relation query, so we have a metadataId-keyed cache that matches the change-log key space. * `JcasbinChangePoller`: scheduled thread that drains `entity_change_log` and `owner_meta` change rows since a high-water cursor and invalidates the affected `metadataIdCache` / `ownerRelCache` keys. Documents the id-cursor in-flight-commit trade-off and the writer-side pre-mutation-name contract. * `JcasbinAuthorizer`: - wires the two GravitinoCaches, the lookups facade, and the poller in `initialize()` / `close()`; - routes `isOwner`, `authorizeByJcasbin` (OWNER path) and `handleMetadataOwnerChange` through the lookups; - drops the old in-class `OwnerInfo` (replaced by `org.apache.gravitino.storage.relational.po.auth.OwnerInfo`) and the `loadOwnerPolicy` / `checkOwnership` pair, collapsed into a single `ownerMatchesUserOrGroups` helper that consumes the resolved `Optional<OwnerInfo>`; - implements `handleEntityStructuralChange` to invalidate (or prefix invalidate) `metadataIdCache` on rename / drop. * `GravitinoAuthorizer`: adds the `handleEntityStructuralChange` default method so callers can wire the new hook without breaking existing implementations. * `Configs`: adds `metadataIdCacheSize` and `changePollIntervalSecs`. The version-validated role caches and the JcasbinRoleLoader rewrite ride on top of this change in a follow-up PR. Test plan --------- `./gradlew :server-common:test --tests 'org.apache.gravitino.server.authorization.*' -PskipITs` `./gradlew :server-common:javadoc :core:javadoc` Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nAuthorizer Builds on top of apache#11117 (eventual-consistency invalidation): adds the strong-consistency role-loading layer. With this PR landed, every cache in JcasbinAuthorizer has an explicit consistency story. What lands here --------------- - `CachedUserRoles` / `CachedGroupRoles`: small immutable snapshots carrying the {role ids, source updated_at} pair used as the staleness sentinel. - `JcasbinLoadedRolesCache`: extracts the previously-inline `LoadedRolesCache` to its own file. Wraps a Caffeine cache with a synchronous removal listener so that `loadedRoles` eviction always flushes the role's JCasbin policies from both enforcers. - `JcasbinAuthorizer` now: - keeps `userRoleCache` and `groupRoleCache` version-validated against `user_meta.updated_at` / `group_meta.updated_at` (probes per request via `loadUserInfo` / `loadGroupInfo`, with per-request dedup through `AuthorizationRequestContext`); - changes `loadedRoles` from `<Long, Boolean>` to `<Long, Long>` (storing `role_meta.updated_at`) so role-policy reloads happen on DB-version change, not TTL expiry; - replaces the `Executor` + `CompletableFuture` async role-load path with a synchronous 4-step `loadRolePrivilege`: 1. version-validated user-direct roles 2. version-validated group-inherited roles for every group in the current `UserPrincipal` 3. prune stale g-rows (IdP group removal / role unassignment) 4. batch `role_meta` version check + reload of stale policies - reuses `loadUserInfo`'s per-request cache from `isMetalakeUser` / `isOwner` so a single HTTP request never re-queries `user_meta`. - `Configs.GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE` doc now notes that the same value sizes user-role, group-role and loaded-role caches. - Lombok wired in `server-common/build.gradle.kts` for the cached snapshot POJOs. Test plan --------- `./gradlew :server-common:test --tests 'org.apache.gravitino.server.authorization.*' -PskipITs` `./gradlew :server-common:javadoc :core:javadoc` Tests cover: version-validated user-role cache, group-inherited role, stale group skipped, group-role revocation, multi-group partial revocation, IdP group removal pruning g-rows, deny-wins over group-inherited allow, role-shared-by-user-and-group survives single-side revocation, plus the existing owner, isSelf, and hasMetadataPrivilegePermission flows. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
40a5dfb to
62ae012
Compare
|
We need to merge #11117 first before moving this PR forward, so I will convert this one to a draft. |
…factor Conflicts in JcasbinAuthorizationLookups.java and JcasbinAuthorizer.java are resolved by composing the two parallel changes: - Take origin/main's hierarchical-schema refactor of `isOwner` / `internalAuthorize` (apache#9970): null-check on metadataObject, walk the schema inheritance chain via the new `buildSchemaInheritanceChain`, and the extracted `isOwnerOfObject` / `authorizeObject` / `authorizeByJcasbin` decomposition. - Take origin/main's `Optional<Long> resolveMetadataId` + negative-cache- avoiding `loadMetadataId` so missing objects are never cached. - Keep this branch's version-validated cache layer: `userRoleCache`, `groupRoleCache`, `loadedRoles`, the `loadUserInfo` / `loadGroupInfo` per-request dedup, and the 4-step `loadRolePrivilege` + `versionCheckAndLoadRoles` path. These replace the previous async addRoleForUserAndLoadPolicies pipeline. - Adapt `ownerMatchesUserOrGroups` to the new `Principal`-based signature from origin/main while routing the user-id lookup through this branch's cached `loadUserInfo` (instead of the direct `getUserEntity` call), so the hot path still hits the version-validated cache. - Drop the now-redundant `resolveFreshMetadataId` in favour of origin/main's inline `MetadataIdConverter.getID(...).ifPresent(...)`. All four jcasbin test classes pass post-merge. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
| private List<Long> loadUserRoles( | ||
| String metalake, String username, long userId, UserUpdatedAt userInfo) { | ||
| String userCacheKey = JcasbinAuthorizationCacheKeys.userRoleKey(metalake, username); | ||
| Optional<CachedUserRoles> cachedOpt = userRoleCache.getIfPresent(userCacheKey); | ||
|
|
||
| if (cachedOpt.isPresent() && cachedOpt.get().getUpdatedAt() >= userInfo.getUpdatedAt()) { | ||
| // Cache is still valid | ||
| CachedUserRoles cached = cachedOpt.get(); | ||
| bindUserRoles(userId, cached.getRoleIds()); | ||
| return cached.getRoleIds(); | ||
| } |
|
I have no further comments. @yuqi1129 can you fix the Copilot comments? |
| */ | ||
| @Getter | ||
| @AllArgsConstructor | ||
| public class CachedGroupRoles { |
There was a problem hiding this comment.
Could u give a better name for this class? You cached group role relations instead of roles, don't you?
There was a problem hiding this comment.
You cached group role relations instead of roles, don't you?
Yeah, this class stores the group and all the IDs of the roles granted to it.
Could u give a better name for this class?
Do you have any suggestions? The current name is fine with me, and if you have a better name, I would appreciate it.
There was a problem hiding this comment.
CachedGroupRoleRels may be better.
| } | ||
|
|
||
| @VisibleForTesting | ||
| static boolean isMetadataContainer(MetadataObject.Type type) { |
There was a problem hiding this comment.
Could u put this into MetadataObjectUtil? We should have a unified place to define this.
| } | ||
|
|
||
| @VisibleForTesting | ||
| static boolean isMetadataContainer(MetadataObject.Type type) { |
There was a problem hiding this comment.
Maybe we should have a better name. Because table can contain column.
| return requestContext.computeUserInfoIfAbsent( | ||
| cacheKey, | ||
| k -> | ||
| Optional.ofNullable( |
There was a problem hiding this comment.
Do we cache null value? Do we record the creation of metadata objects?
There was a problem hiding this comment.
I do not plan to cache a null value, and let me check whether the code here.
| } | ||
|
|
||
| static String userRoleKey(String metalake, String username) { | ||
| return "USER" + SEPARATOR + metalake + SEPARATOR + username; |
There was a problem hiding this comment.
Should we use metalake + SEPARATOR + USER_ROLE_REL SEPARATOR username?
| } | ||
|
|
||
| // Load full role entity using roleName from the batch query (no extra DB scan) | ||
| RoleEntity roleEntity; |
There was a problem hiding this comment.
Could u batch get the roles?
What changes were proposed in this pull request?
This is the final piece of the #10812 split. It rewrites
JcasbinAuthorizer's strong-consistency caching layer on top of the lookups + change-poller infrastructure introduced in #11117:AuthorizationRequestContext: user identity, group identity and the role-load step are deduplicated within a single HTTP request.userRoleCache,groupRoleCache,loadedRoles): cache hits revalidate againstuser_meta.updated_at/group_meta.updated_at/role_meta.updated_atbefore being trusted, replacing TTL-only invalidation.JcasbinRoleLoader(new): owns the 4-step role load — version-checked user-direct roles, version-checked group-inherited roles, prune stale g-rows, then batch version-check + reload ofrole_meta.JcasbinLoadedRolesCache(new): wraps aGravitinoCachewith a removal listener that synchronously deletes the role's JCasbin policies, so eviction and policy state stay in sync.Executor+CompletableFutureasync role-load path; role loading is now synchronous viaAuthorizationRequestContext.loadRole(Runnable).isMetalakeUsernow takesAuthorizationRequestContextso the per-requestUserUpdatedAtcache populated byauthorize/isOwneris reused, removing a redundant DB round-trip.Why are the changes needed?
Fix: #10772
Does this PR introduce any user-facing change?
No. New configs (
gravitino.authorization.jcasbin.*) have safe defaults.How was this patch tested?
TestGravitinoCache(TTL, max-size eviction, prefix edge cases) andTestAuthorizationRequestContext(request-scoped dedup for user/owner/metadata-id, including absent-result caching).TestJcasbinAuthorizergroup-role / version-validated cases: group-inherited role, IdP group removal, multi-group partial revocation, deny-wins over group-inherited allow, role-shared-by-user-and-group survives single-side revocation.TestJcasbinAuthorizer,TestJcasbinAuthorizerCacheHelpersandTestPassThroughAuthorizersuites still pass.